From: Colin Walters Date: Sun, 2 Aug 2009 07:46:01 +0000 (-0400) Subject: LookingGlass - JavaScript prompt and interactive Clutter controller X-Git-Tag: archive/raspbian/50.5-1+rpi1~1^2~1013^2^2~11170 X-Git-Url: https://dgit.raspbian.org/%22http://www.example.com/cgi/%22/%22http:/www.example.com/cgi/%22?a=commitdiff_plain;h=52ae75d4b8d3d5c1129f2d9db862df197f634ab5;p=gnome-shell.git LookingGlass - JavaScript prompt and interactive Clutter controller Add a dropdown pane triggered by Alt-F2, "lg" which supports interactive JavaScript evaluation, saving/restoring a history file, as well as an inspector element to pick by using the mouse. --- diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js new file mode 100644 index 000000000..cba5920bb --- /dev/null +++ b/js/ui/lookingGlass.js @@ -0,0 +1,470 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Big = imports.gi.Big; +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const Pango = imports.gi.Pango; +const Shell = imports.gi.Shell; +const Signals = imports.signals; +const Lang = imports.lang; +const Mainloop = imports.mainloop; + +const Tweener = imports.ui.tweener; +const Main = imports.ui.main; + +const LG_BORDER_COLOR = new Clutter.Color(); +LG_BORDER_COLOR.from_pixel(0x0000aca0); +const LG_BACKGROUND_COLOR = new Clutter.Color(); +LG_BACKGROUND_COLOR.from_pixel(0x000000d5); +const GREY = new Clutter.Color(); +GREY.from_pixel(0xAFAFAFFF); +const MATRIX_GREEN = new Clutter.Color(); +MATRIX_GREEN.from_pixel(0x88ff66ff); +// FIXME pull from GConf +const MATRIX_FONT = 'Monospace 10'; + + /* Imports...feel free to add here as needed */ +var commandHeader = "const Clutter = imports.gi.Clutter; " + + "const GLib = imports.gi.GLib; " + + "const Gtk = imports.gi.Gtk; " + + "const Mainloop = imports.mainloop; " + + "const Meta = imports.gi.Meta; " + + "const Shell = imports.gi.Shell; " + + "const Main = imports.ui.main; " + + "const Lang = imports.lang; " + + "const Tweener = imports.ui.tweener; " + + /* Utility functions...we should probably be able to use these + * in the shell core code too. */ + "const global = Shell.Global.get(); " + + "const stage = global.stage; " + + "const color = function(pixel) { let c= new Clutter.Color(); c.from_pixel(pixel); return c; }; " + + /* Special lookingGlass functions */ + "const it = Main.lookingGlass.getIt(); " + + "const r = Lang.bind(Main.lookingGlass, Main.lookingGlass.getResult); "; + +function Result(command, o, index) { + this._init(command, o, index); +} + +Result.prototype = { + _init : function(command, o, index) { + this.index = index; + this.o = o; + + this.actor = new Big.Box(); + + let cmdTxt = new Clutter.Text({ color: MATRIX_GREEN, + font_name: MATRIX_FONT, + ellipsize: Pango.EllipsizeMode.END, + text: command }); + this.actor.append(cmdTxt, Big.BoxPackFlags.NONE); + let resultTxt = new Clutter.Text({ color: MATRIX_GREEN, + font_name: MATRIX_FONT, + ellipsize: Pango.EllipsizeMode.END, + text: "r(" + index + ") = " + o }); + this.actor.append(resultTxt, Big.BoxPackFlags.NONE); + let line = new Big.Box({ opacity: 0, border_color: GREY, + border_bottom: 1, + height: 4 }); + this.actor.append(line, Big.BoxPackFlags.NONE); + } +} + +function ActorHierarchy() { + this._init(); +} + +ActorHierarchy.prototype = { + _init : function () { + this._previousTarget = null; + this._target = null; + + this._parentList = []; + + this.actor = new Big.Box({ spacing: 4, + border: 1, + padding: 4, + border_color: GREY }); + }, + + setTarget: function(actor) { + this._previousTarget = this._target; + this.target = actor; + + this.actor.remove_all(); + + /* FIXME - need scrolling here */ + return; + + if (this.target == null) + return; + + this._parentList = []; + let parent = actor; + while ((parent = parent.get_parent()) != null) { + this._parentList.push(parent); + + let link = new Clutter.Text({ color: MATRIX_GREEN, + font_name: MATRIX_FONT, + reactive: true }); + this.actor.append(link, Big.BoxPackFlags.IF_FITS); + let parentTarget = parent; + link.connect('button-press-event', Lang.bind(this, function () { + this._selectByActor(parentTarget); + })); + } + this.emit('selection', actor); + }, + + _selectByActor: function(actor) { + let idx = this._parentList.indexOf(actor); + let children = this.actor.get_children(); + let link = children[idx]; + this.emit('selection', actor); + } +} +Signals.addSignalMethods(ActorHierarchy.prototype); + +function PropertyInspector() { + this._init(); +} + +PropertyInspector.prototype = { + _init : function () { + this._target = null; + + this._parentList = []; + + this.actor = new Big.Box({ spacing: 4, + border: 1, + padding: 4, + border_color: GREY }); + }, + + setTarget: function(actor) { + this.target = actor; + + this.actor.remove_all(); + + /* FIXME - need scrolling here */ + return; + + for (let propName in actor) { + let valueStr; + try { + valueStr = "" + actor[propName]; + } catch (e) { + valueStr = ''; + } + let propText = propName + ": " + valueStr; + let propDisplay = new Clutter.Text({ color: MATRIX_GREEN, + font_name: MATRIX_FONT, + reactive: true, + text: propText }); + this.actor.append(propDisplay, Big.BoxPackFlags.IF_FITS); + } + } +} + +function LookingGlass() { + this._init(); +} + +LookingGlass.prototype = { + _init : function() { + let global = Shell.Global.get(); + + this._idleHistorySaveId = 0; + let historyPath = Shell.Global.get().configdir + "/lookingglass-history.txt"; + this._historyFile = Gio.file_new_for_path(historyPath); + this._savedText = null; + this._historyNavIndex = -1; + this._history = []; + this._readHistory(); + + this._open = false; + + this._offset = 0; + this._results = []; + + // TODO replace with scrolling or something better + this._maxItems = 10; + + this.actor = new Big.Box({ background_color: LG_BACKGROUND_COLOR, + border: 1, + border_color: LG_BORDER_COLOR, + corner_radius: 4, + padding_top: 8, + padding_left: 4, + padding_right: 4, + padding_bottom: 4, + spacing: 4, + visible: false + }); + global.stage.add_actor(this.actor); + + let toolbar = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, + border: 1, border_color: GREY, + corner_radius: 4 }); + this.actor.append(toolbar, Big.BoxPackFlags.NONE); + let inspectIcon = Shell.TextureCache.get_default().load_gicon(new Gio.ThemedIcon({ name: 'gtk-color-picker' }), + 24); + toolbar.append(inspectIcon, Big.BoxPackFlags.NONE); + inspectIcon.reactive = true; + inspectIcon.connect('button-press-event', Lang.bind(this, function () { + let global = Shell.Global.get(); + let width = 150; + let eventHandler = new Big.Box({ background_color: LG_BACKGROUND_COLOR, + border: 1, + border_color: LG_BORDER_COLOR, + corner_radius: 4, + y: global.stage.height/2, + reactive: true + }); + eventHandler.connect('notify::allocation', Lang.bind(this, function () { + eventHandler.x = Math.floor((global.stage.width)/2 - (eventHandler.width)/2); + })); + global.stage.add_actor(eventHandler); + let displayText = new Clutter.Text({ color: MATRIX_GREEN, + font_name: MATRIX_FONT, text: '' }); + eventHandler.append(displayText, Big.BoxPackFlags.EXPAND); + eventHandler.connect('button-press-event', Lang.bind(this, function (actor, event) { + let global = Shell.Global.get(); + Clutter.ungrab_pointer(eventHandler); + + let [stageX, stageY] = event.get_coords(); + let target = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, + stageX, + stageY); + this._pushResult('', + target); + this._hierarchy.setTarget(target); + eventHandler.destroy(); + this.actor.show(); + let global = Shell.Global.get(); + global.stage.set_key_focus(this._entry); + return true; + })); + eventHandler.connect('motion-event', Lang.bind(this, function (actor, event) { + let global = Shell.Global.get(); + let [stageX, stageY] = event.get_coords(); + let target = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, + stageX, + stageY); + displayText.text = ' ' + target; + return true; + })); + Clutter.grab_pointer(eventHandler); + this.actor.hide(); + return true; + })); + + this._mainContent = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, + spacing: 4 }); + this.actor.append(this._mainContent, Big.BoxPackFlags.EXPAND); + + this._resultsArea = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, + spacing: 0 }); + this._mainContent.append(this._resultsArea, Big.BoxPackFlags.EXPAND); + + let entryArea = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL }); + + let label = new Clutter.Text({ color: MATRIX_GREEN, + font_name: MATRIX_FONT, + text: 'js>>> ' }); + entryArea.append(label, Big.BoxPackFlags.NONE); + + this._entry = new Clutter.Text({ color: MATRIX_GREEN, + font_name: MATRIX_FONT, + editable: true, + activatable: true, + singleLineMode: true, + text: ''}); + entryArea.append(this._entry, Big.BoxPackFlags.EXPAND); + this.actor.append(entryArea, Big.BoxPackFlags.NONE); + + let inspectionBox = new Big.Box({ spacing: 4 }); + this._mainContent.append(inspectionBox, Big.BoxPackFlags.NONE); + + this._hierarchy = new ActorHierarchy(); + inspectionBox.append(this._hierarchy.actor, Big.BoxPackFlags.EXPAND); + this._propInspector = new PropertyInspector(); + inspectionBox.append(this._propInspector.actor, Big.BoxPackFlags.EXPAND); + this._hierarchy.connect('selected', Lang.bind(this, function (h, actor) { + this._propInspector.setTarget(actor); + })); + + this._entry.connect('activate', Lang.bind(this, function (o, e) { + let text = o.get_text(); + // Ensure we don't get newlines in the command; the history file is + // newline-separated. + text.replace('\n', ' '); + this._evaluate(text); + this._historyNavIndex = -1; + return true; + })); + this._entry.connect('key-press-event', Lang.bind(this, function(o, e) { + let symbol = Shell.get_event_key_symbol(e); + if (symbol == Clutter.Escape) { + this.close(); + return true; + } else if (symbol == Clutter.Up) { + if (this._historyNavIndex >= this._history.length - 1) + return true; + this._historyNavIndex++; + if (this._historyNavIndex == 0) + this._savedText = this._entry.text; + this._entry.text = this._history[this._history.length - this._historyNavIndex - 1]; + return true; + } else if (symbol == Clutter.Down) { + if (this._historyNavIndex <= 0) + return true; + this._historyNavIndex--; + if (this._historyNavIndex < 0) + this._entry.text = this._savedText; + else + this._entry.text = this._history[this._history.length - this._historyNavIndex - 1]; + return true; + } else { + this._historyNavIndex = -1; + this._savedText = null; + return false; + } + })); + }, + + _readHistory: function () { + if (!this._historyFile.query_exists(null)) + return; + let [result, contents, length, etag] = this._historyFile.load_contents(null); + this._history = contents.split('\n'); + }, + + _queueHistorySave: function() { + if (this._idleHistorySaveId > 0) + return; + this._idleHistorySaveId = Mainloop.timeout_add_seconds(30, Lang.bind(this, this._doSaveHistory)); + }, + + _doSaveHistory: function () { + this._idleHistorySaveId = false; + let output = this._historyFile.replace(null, true, Gio.FileCreateFlags.NONE, null); + let dataOut = new Gio.DataOutputStream({ base_stream: output }); + dataOut.put_string(this._history.join('\n'), null); + dataOut.put_string('\n', null); + dataOut.close(null); + return false; + }, + + _pushResult: function(command, obj) { + let index = this._results.length + this._offset; + let result = new Result('>>> ' + command, obj, index); + this._results.push(result); + this._resultsArea.append(result.actor, Big.BoxPackFlags.NONE); + this._propInspector.setTarget(obj); + let children = this._resultsArea.get_children(); + if (children.length > this._maxItems) { + this._results.shift(); + children[0].destroy(); + this._offset++; + } + this._it = obj; + }, + + _evaluate : function(command) { + this._history.push(command); + this._queueHistorySave(); + + let fullCmd = commandHeader + command; + + let resultObj; + try { + resultObj = eval(fullCmd); + } catch (e) { + resultObj = ""; + } + + this._pushResult(command, resultObj); + this._hierarchy.setTarget(null); + this._entry.text = ''; + }, + + getIt: function () { + return this._it; + }, + + getResult: function(idx) { + return this._results[idx - this._offset].o; + }, + + toggle: function() { + if (this._open) + this.close(); + else + this.open(); + }, + + _resizeTo: function(actor) { + let stage = Shell.Global.get().stage; + let stageWidth = stage.width; + let myWidth = stage.width * 0.7; + let myHeight = stage.height * 0.7; + let [srcX, srcY] = actor.get_transformed_position(); + this.actor.x = srcX + (stage.width-myWidth)/2; + this._hiddenY = srcY + actor.height - myHeight - 4; // -4 to hide the top corners + this._targetY = this._hiddenY + myHeight; + this.actor.y = this._hiddenY; + this.actor.width = myWidth; + this.actor.height = myHeight; + }, + + slaveTo: function(actor) { + this._slaveTo = actor; + actor.connect('notify::allocation', Lang.bind(this, function () { + this._resizeTo(actor); + })); + this._resizeTo(actor); + }, + + open : function() { + if (this._open) + return; + + this.actor.show(); + this.actor.lower(Main.chrome.actor); + this._open = true; + + Tweener.removeTweens(this.actor); + + if (!Main.startModal()) + return; + + let global = Shell.Global.get(); + global.stage.set_key_focus(this._entry); + + Tweener.addTween(this.actor, { time: 0.5, + transition: "easeOutQuad", + y: this._targetY + }); + }, + + close : function() { + if (!this._open) + return; + + this._historyNavIndex = -1; + this._open = false; + Tweener.removeTweens(this.actor); + + Main.endModal(); + + Tweener.addTween(this.actor, { time: 0.5, + transition: "easeOutQuad", + y: this._hiddenY, + onComplete: Lang.bind(this, function () { + this.actor.hide(); + }) + }); + } +}; +Signals.addSignalMethods(LookingGlass.prototype); diff --git a/js/ui/main.js b/js/ui/main.js index f87f2894f..d22d52393 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -13,6 +13,7 @@ const Chrome = imports.ui.chrome; const Overlay = imports.ui.overlay; const Panel = imports.ui.panel; const RunDialog = imports.ui.runDialog; +const LookingGlass = imports.ui.lookingGlass; const Sidebar = imports.ui.sidebar; const Tweener = imports.ui.tweener; const WindowManager = imports.ui.windowManager; @@ -25,6 +26,7 @@ let panel = null; let sidebar = null; let overlay = null; let runDialog = null; +let lookingGlass = null; let wm = null; let recorder = null; let inModal = false; @@ -142,6 +144,14 @@ function endModal() { inModal = false; } +function createLookingGlass() { + if (lookingGlass == null) { + lookingGlass = new LookingGlass.LookingGlass(); + lookingGlass.slaveTo(panel.actor); + } + return lookingGlass; +} + function createAppLaunchContext() { let global = Shell.Global.get(); let screen = global.screen; diff --git a/js/ui/runDialog.js b/js/ui/runDialog.js index 2334b5b86..9e84782a8 100644 --- a/js/ui/runDialog.js +++ b/js/ui/runDialog.js @@ -31,7 +31,12 @@ RunDialog.prototype = { this._isOpen = false; - this._internalCommands = { 'restart': Lang.bind(this, function() { + this._internalCommands = { 'lg': + Lang.bind(this, function() { + Mainloop.idle_add(function() { Main.createLookingGlass().open(); }); + }), + + 'restart': Lang.bind(this, function() { let global = Shell.Global.get(); global.reexec_self(); })